868
//
// (C) Copyright 2009 Irantha Suwandarathna (irantha@gmail.com)
// All rights reserved.
//

/* Copyright (c) 1995-2000, The Hypersonic SQL Group.
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the Hypersonic SQL Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL THE HYPERSONIC SQL GROUP,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 *
 * This software consists of voluntary contributions made by many individuals
 * on behalf of the Hypersonic SQL Group.
 *
 *
 * For work added by the HSQL Development Group:
 *
 * Copyright (c) 2001-2008, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use _in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions _in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer _in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


using System;
using System.Collections.Generic;
using System.Text;
using EffiProz.Core.Persist;
using EffiProz.Core.Lib;
using EffiProz.Core.Rights;
using EffiProz.Core.Errors;
using EffiProz.Core.Results;
using EffiProz.Core.DataTypes;
using EffiProz.Core.DbInfos;


namespace EffiProz.Core
{

    // fredt@users 20020130 - patch 476694 by velichko - transaction savepoints
    // additions to different parts to support savepoint transactions
    // fredt@users 20020215 - patch 1.7.0 - new HsqlProperties class
    // support use of properties from database.properties file
    // fredt@users 20020218 - patch 1.7.0 - DEFAULT keyword
    // support for default values for table columns
    // fredt@users 20020305 - patch 1.7.0 - restructuring
    // some methods move to Table.java, some removed
    // fredt@users 20020221 - patch 513005 by sqlbob@users (RMP) - restructuring
    // fredt@users 20020221 - patch 513005 by sqlbob@users (RMP) - error trapping
    // boucherb@users 20020130 - patch 1.7.0 - use lookup for speed
    // idents listed _in alpha-order for easy check of stats...
    // fredt@users 20020420 - patch523880 by leptipre@users - VIEW support
    // boucherb@users - doc 1.7.0 - added javadoc comments
    // tony_lai@users 20020820 - patch 595073 - duplicated exception msg
    // tony_lai@users 20020820 - changes to shutdown compact to save memory
    // boucherb@users 20020828 - allow reconnect to local db that has shutdown
    // fredt@users 20020912 - patch 1.7.1 by fredt - drop duplicate name triggers
    // fredt@users 20021112 - patch 1.7.2 by Nitin Chauhan - use of switch
    // rewrite of the majority of multiple if(){}else if(){} chains with switch()
    // boucherb@users 20020310 - class loader update for JDK 1.1 compliance
    // fredt@users 20030401 - patch 1.7.2 by akede@users - data files _readonly
    // fredt@users 20030401 - patch 1.7.2 by Brendan Ryan - data files _in Jar
    // boucherb@users 20030405 - removed 1.7.2 lint - updated JavaDocs
    // boucherb@users 20030425 - DDL methods are moved to DatabaseCommandInterpreter.java
    // boucherb@users - fredt@users 200305..200307 - patch 1.7.2 - DatabaseManager upgrade
    // loosecannon1@users - patch 1.7.2 - properties on the JDBC URL
    // oj@openoffice.org - changed to file access api

    /**
     *  Database is the root class for HSQL Database Engine database. <p>
     *
     *  It holds the data structure that form an HSQLDB database instance.
     *
     * Modified significantly from the Hypersonic original _in successive
     * HSQLDB versions.
     *
     * @author Thomas Mueller (Hypersonic SQL Group)
     * @version 1.8.0
     * @since Hypersonic SQL
     */
    public class Database
    {

        public int databaseID;
        String databaseUniqueName;
        String databaseType;
        private String canonicalPath;

        // loosecannon1@users 1.7.2 patch properties on the JDBC URL
        public EffiProzProperties urlProperties;
        private String path;
        public DatabaseInformation dbInfo;

        /** indicates the state of the database */
        private int dbState;
        public Logger logger;

        /** true means that all tables are readonly. */
        public bool databaseReadOnly;

        /**
         * true means that all CACHED and TEXT tables are readonly.
         *  MEMORY tables are updatable but updates are not persisted.
         */
        private bool filesReadOnly;

        /** true means filesReadOnly but CACHED and TEXT tables are disallowed */
        private bool filesInJar;
        public bool sqlEnforceSize;
        public bool sqlEnforceNames;
        private bool bIgnoreCase;
        private bool _isReferentialIntegrity;
        public EffiProzDatabaseProperties databaseProperties;
        private bool shutdownOnNoConnection;
        int resultMaxMemoryRows;

        // schema invarient objects
        public UserManager userManager;
        public GranteeManager granteeManager;
        public QNameManager nameManager;

        // session related objects
        public SessionManager sessionManager;
        public TransactionManager txManager;

        // schema objects
        public SchemaManager schemaManager;

        //
        public PersistentStoreCollectionDatabase persistentStoreCollection;

        //
        public LobManager lobManager;
        public Collation collation;

        //
        public const int DATABASE_ONLINE = 1;
        public const int DATABASE_OPENING = 4;
        public const int DATABASE_CLOSING = 8;
        public const int DATABASE_SHUTDOWN = 16;
        public const int CLOSEMODE_IMMEDIATELY = -1;
        public const int CLOSEMODE_NORMAL = 0;
        public const int CLOSEMODE_COMPACT = 1;
        public const int CLOSEMODE_SCRIPT = 2;

        //
        public const int LOCKS = 0;
        public const int MVLOCKS = 1;
        public const int MVCC = 2;

        /**
         *  Constructs a new Database object.
         *
         * @param type is the type of the database: "mem:", "file:", "res:"
         * @param path is the given path to the database files
         * @param canonicalPath is the canonical path
         * @param props property overrides placed on the connect URL
         * @exception  HsqlException if the specified name and path
         *      combination is illegal or unavailable, or the database files the
         *      name and path resolves to are in use by another process
         */
        public Database(String type, String path, String canonicalPath,
                 EffiProzProperties props)
        {

            setState(Database.DATABASE_SHUTDOWN);

            this.databaseType = type;
            this.path = path;
            this.canonicalPath = canonicalPath;
            this.urlProperties = props;

            if (databaseType == DatabaseURL.S_RES)
            {
                filesInJar = true;
                filesReadOnly = true;
            }

            logger = new Logger(this);
            shutdownOnNoConnection =
                urlProperties.isPropertyTrue(EffiProzDatabaseProperties.url_shutdown);
            lobManager = new LobManager(this);
        }

        /**
         * Opens this database.  The database should be opened after construction.
         */
        public void open()
        {
            lock (this)
            {
                if (!isShutdown())
                {
                    return;
                }

                reopen();
            }
        }

        /**
         * Opens this database.  The database should be opened after construction.
         * or reopened by the close(int closemode) method during a
         * "shutdown compact". Closes the log if there is an error.
         */
        void reopen()
        {

            bool isNew = false;

            setState(DATABASE_OPENING);

            try
            {
                nameManager = new QNameManager(this);
                granteeManager = new GranteeManager(this);
                userManager = new UserManager(this);
                schemaManager = new SchemaManager(this);
                persistentStoreCollection =
                    new PersistentStoreCollectionDatabase();
                _isReferentialIntegrity = true;
                sessionManager = new SessionManager(this);
                collation = Collation.getDefaultInstance();
                dbInfo = DatabaseInformation.newDatabaseInformation(this);
                txManager = new TransactionManagerMVCC(this);

                lobManager.createSchema();
                logger.openPersistence();

                isNew = logger.isNewDatabase;

                if (isNew)
                {
                    userManager.createSAUser();
                    schemaManager.createPublicSchema();
                    lobManager.initialiseLobSpace();
                    logger.checkpoint(false);
                }

                lobManager.open();
                dbInfo.setWithContent(true);
            }
            catch (Exception e)
            {
                logger.closePersistence(Database.CLOSEMODE_IMMEDIATELY);
                logger.releaseLock();
                setState(DATABASE_SHUTDOWN);
                clearStructures();
                DatabaseManager.removeDatabase(this);

                if (!(e is CoreException))
                {
                    e = Error.error(ErrorCode.GENERAL_ERROR, e);
                }

                logger.logSevereEvent("could not reopen database", e);

                throw (CoreException)e;
            }

            setState(DATABASE_ONLINE);
        }

        /**
         * Clears the data structuress, making them elligible for garbage collection.
         */
        void clearStructures()
        {

            if (schemaManager != null)
            {
                schemaManager.clearStructures();
            }

            granteeManager = null;
            userManager = null;
            nameManager = null;
            schemaManager = null;
            sessionManager = null;
            dbInfo = null;
        }

        /**
         *  Returns the database ID.
         */
        public int getDatabaseID()
        {
            return this.databaseID;
        }

        /**
         * Returns a unique String identifier for the database.
         */
        public String getUniqueName()
        {
            return databaseUniqueName;
        }

        public void setUniqueName(String name)
        {
            databaseUniqueName = name;
        }

        /**
         *  Returns the type of the database: "mem", "file", "res"
         */
        public String getType()
        {
            return databaseType;
        }

        /**
         *  Returns the path of the database
         */
        public String getPath()
        {
            return path;
        }

        public QNameManager.QName getCatalogName()
        {
            return nameManager.getCatalogName();
        }

        /**
         *  Returns the database properties.
         */
        public EffiProzDatabaseProperties getProperties()
        {
            return databaseProperties;
        }

        /**
         * Returns the SessionManager for the database.
         */
        public SessionManager getSessionManager()
        {
            return sessionManager;
        }

        public bool isReadOnly()
        {
            return databaseReadOnly;
        }

        /**
         *  Returns true if database has been shut down, false otherwise
         */
        public bool isShutdown()
        {
            lock (this)
            {
                return dbState == DATABASE_SHUTDOWN;
            }
        }

        /**
         *  Constructs a new Session that operates within (is connected to) the
         *  context of this Database object. <p>
         *
         *  If successful, the new Session object initially operates on behalf of
         *  the user specified by the supplied user name.
         *
         * Throws if username or password is invalid.
         */
        public Session connect(String username, String password,
                                     String zoneString, int timeZoneSeconds)
        {
            lock (this)
            {

                if (username.Equals("SA", StringComparison.OrdinalIgnoreCase))
                {
                    username = "SA";
                }

                User user = userManager.getUser(username, password);
                Session session = sessionManager.newSession(this, user,
                    databaseReadOnly, false, zoneString, timeZoneSeconds);

                return session;
            }
        }

        /**
         *  Puts this Database object in global read-only mode. After
         *  this call, all existing and future sessions are limited to read-only
         *  transactions. Any following attempts to update the state of the
         *  database will result in throwing an HsqlException.
         */
        public void setReadOnly()
        {
            databaseReadOnly = true;
            filesReadOnly = true;
        }

        /**
         * After this call all CACHED and TEXT tables will be set to read-only
         * mode. Changes to MEMORY tables will NOT
         * be stored or updated in the script file. This mode is intended for
         * use with read-only media where data should not be persisted.
         */
        public void setFilesReadOnly()
        {
            filesReadOnly = true;
        }

        /**
         * Is this in filesReadOnly mode?
         */
        public bool isFilesReadOnly()
        {
            return filesReadOnly;
        }

        /**
         * Is this in filesInJar mode?
         */
        public bool isFilesInJar()
        {
            return filesInJar;
        }

        /**
         *  Returns the UserManager for this Database.
         */
        public UserManager getUserManager()
        {
            return userManager;
        }

        /**
         *  Returns the GranteeManager for this Database.
         */
        public GranteeManager getGranteeManager()
        {
            return granteeManager;
        }

        /**
         *  Sets the _isReferentialIntegrity attribute.
         */
        public void setReferentialIntegrity(bool _ref)
        {
            _isReferentialIntegrity = _ref;
        }

        /**
         *  Is referential integrity currently enforced?
         */
        public bool isReferentialIntegrity()
        {
            return _isReferentialIntegrity;
        }

        /**
         * Sets the database to treat any new VARCHAR column declarations as
         * VARCHAR_IGNORECASE.
         */
        public void setIgnoreCase(bool b)
        {
            bIgnoreCase = b;
        }

        /**
         *  Does the database treat any new VARCHAR column declarations as
         * VARCHAR_IGNORECASE.
         */
        public bool isIgnoreCase()
        {
            return bIgnoreCase;
        }

        public int getResultMaxMemoryRows()
        {
            return resultMaxMemoryRows;
        }

        public void setResultMaxMemoryRows(int size)
        {
            resultMaxMemoryRows = size;
        }

        public void setStrictNames(bool mode)
        {
            sqlEnforceNames = mode;
        }

        public void setStrictColumnSize(bool mode)
        {
            sqlEnforceSize = mode;
        }

        /**
         *  Called by the garbage collector on this Databases object when garbage
         *  collection determines that there are no more references to it.
         */
        protected void finalize()
        {

            if (getState() != DATABASE_ONLINE)
            {
                return;
            }

            try
            {
                close(CLOSEMODE_IMMEDIATELY);
            }
            catch (CoreException)
            {    // it's too late now
            }
        }

        private static DateTime checkPointTime = DateTime.MinValue;
        private static object checkPointLock = new object();

        public void checkPointIfLast()
        {

            if (shutdownOnNoConnection && sessionManager.isEmpty()
                    && dbState == DATABASE_ONLINE)
            {
                ScheduleCheckpoint();
            }
        }

        /// <summary>
        /// This method delays checkpoints for half a second
        /// so if another checkpoint or connection happens 
        /// it won't lock the database much.
        /// I've tested this quickly and if this code is removed
        /// scalability drops down drammatically.
        /// User can completely control this by setting the property
        /// shutdownOnNoConnection to false
        /// </summary>
        private void ScheduleCheckpoint()
        {
            lock (checkPointLock)
            {
                if (checkPointTime > DateTime.Now)
                {
                    checkPointTime.AddMilliseconds(500);
                }
                else
                {
                    checkPointTime = DateTime.Now.AddMilliseconds(500);
                    System.Threading.ThreadPool.QueueUserWorkItem(
                        (o) =>
                        {
                            while (DateTime.Now < checkPointTime)
                            {
                                System.Threading.Thread.Sleep(10);
                            }

                        }
                    );
                }
            }
        }

        /**
         *  Closes this Database using the specified mode. <p>
         *
         * <ol>
         *  <LI> closemode -1 performs SHUTDOWN IMMEDIATELY, equivalent
         *       to a poweroff or crash.
         *
         *  <LI> closemode 0 performs a normal SHUTDOWN that
         *      checkpoints the database normally.
         *
         *  <LI> closemode 1 performs a shutdown compact that scripts
         *       out the contents of any CACHED tables to the log then
         *       deletes the existing *.data file that contains the data
         *       for all CACHED table before the normal checkpoint process
         *       which in turn creates a new, compact *.data file.
         * </ol>
         */
        public void close(int closemode)
        {

            CoreException he = null;

            setState(DATABASE_CLOSING);
            sessionManager.closeAllSessions();
            sessionManager.clearAll();

            if (filesReadOnly)
            {
                closemode = CLOSEMODE_IMMEDIATELY;
            }

            /**
             * @todo  fredt - impact of possible error conditions in closing the log
             * should be investigated for the CLOSEMODE_COMPACT mode
             */
            logger.closePersistence(closemode);
            lobManager.close();

            try
            {
                if (closemode == CLOSEMODE_COMPACT)
                {
                    clearStructures();
                    reopen();
                    setState(DATABASE_CLOSING);
                    logger.closePersistence(CLOSEMODE_NORMAL);
                }
            }
            catch (Exception t)
            {
                if (t is CoreException)
                {
                    he = (CoreException)t;
                }
                else
                {
                    he = Error.error(ErrorCode.GENERAL_ERROR, t.Message);
                }
            }

            logger.releaseLock();
            setState(DATABASE_SHUTDOWN);
            clearStructures();

            // fredt - this could change to avoid removing a db from the
            // DatabaseManager repository if there are pending getDatabase()
            // calls
            DatabaseManager.removeDatabase(this);

            if (he != null)
            {
                throw he;
            }
        }

        /**
         * Ensures system table producer's table cache, if it exists, is set dirty.
         * After this call up-to-date versions are generated in response to
         * system table requests. <p>
         *
         * Also resets all prepared statements if a change to database structure
         * can possibly affect any existing prepared statement's validity.<p>
         *
         * The argument is false if the change to the database structure does not
         * affect the prepared statement, such as when a new table is added.<p>
         *
         * The argument is typically true when a database object is dropped,
         * altered or a permission was revoked.
         *
         * @param  resetPrepared If true, reset all prepared statements.
         */
        public void setMetaDirty(bool resetPrepared)
        {

            if (dbInfo != null)
            {
                dbInfo.setDirty();
            }
        }

        private void setState(int state)
        {
            lock (this)
            {
                dbState = state;
            }
        }

        public int getState()
        {
            lock (this)
            {
                return dbState;
            }
        }

        String getStateString()
        {

            int state = getState();

            switch (state)
            {

                case DATABASE_CLOSING:
                    return "DATABASE_CLOSING";

                case DATABASE_ONLINE:
                    return "DATABASE_ONLINE";

                case DATABASE_OPENING:
                    return "DATABASE_OPENING";

                case DATABASE_SHUTDOWN:
                    return "DATABASE_SHUTDOWN";

                default:
                    return "UNKNOWN";
            }
        }

        public String[] getSettingsSQL()
        {

            List<string> list = new List<string>();

            if (!getCatalogName().name.Equals(
                    QNameManager.DEFAULT_CATALOG_NAME))
            {
                String name = getCatalogName().statementName;

                list.Add("ALTER CATALOG PUBLIC RENAME TO " + name);
            }

            if (collation.collator != null)
            {
                String name = collation.getName().statementName;

                list.Add("SET DATABASE COLLATION " + name);
            }

            return list.ToArray();
        }

        /**
         * Returns the schema and authorisation statements for the database.
         */
        public Result getScript(bool indexRoots)
        {

            Result r = Result.newSingleColumnResult("COMMAND", SqlType.SQL_VARCHAR);

            // properties
            String[] list = logger.getPropertiesSQL();

            addRows(r, list);

            list = getSettingsSQL();

            addRows(r, list);

            list = getGranteeManager().getSQL();

            addRows(r, list);

            // schemas and schema objects such as tables, sequences, etc.
            list = schemaManager.getSQLArray();

            addRows(r, list);

            // user session start schema names
            list = getUserManager().getInitialSchemaSQL();

            addRows(r, list);

            // grantee rights
            list = getGranteeManager().getRightstSQL();

            addRows(r, list);

            // index roots
            if (indexRoots)
            {
                list = schemaManager.getIndexRootsSQL();

                addRows(r, list);
            }

            // text headers
            list = schemaManager.getTextTableSQL(!indexRoots);

            addRows(r, list);

            return r;
        }

        private static void addRows(Result r, String[] sql)
        {

            if (sql == null)
            {
                return;
            }

            for (int i = 0; i < sql.Length; i++)
            {
                String[] s = new String[1];

                s[0] = sql[i];

                r.initialiseNavigator().add(s);
            }
        }

        // boucherb@users - 200403?? - patch 1.7.2 - metadata
        //------------------------------------------------------------------------------

        /**
         * Retrieves the uri portion of this object's in-process JDBC url.
         *
         * @return the uri portion of this object's in-process JDBC url
         */
        public String getURI()
        {
            return databaseType + canonicalPath;
        }

        public String getCanonicalPath()
        {
            return canonicalPath;
        }

        // oj@openoffice.org - changed to file access api
        public EffiProzProperties getURLProperties()
        {
            return urlProperties;
        }

    }
}
